
package com.epam.android.training.task.rss;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.nio.CharBuffer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author pavel_busko
 * 
 * Handler to asynchronously parse RSS feed. Override callback methods
 * {@link #onChannelInfoReady(RssItem)} and {@link #onItemReady(RssItem)} to
 * control parsing flow.
 * 
 * It isn't thread-safe so create separate instances for concurrent parse
 * operations.
 */
public class RssHandler extends DefaultHandler {

    private RssItem item;

    /**
     * Usual RSS pubDate format.
     */
    private final DateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");

    private final StringBuilder currentCharacters = new StringBuilder();

    private boolean angleBracketMatched = false;

    public void onChannelInfoReady(RssItem item) {}

    public void onItemReady(RssItem item) {}

    public void onException(ParseException e) {}

    @Override
    public void startDocument() throws SAXException {
        // an item to store channel currentValue (channel and item have equal
        // elements)
        item = new RssItem();
    }

    @Override
    public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
            throws SAXException {
        Element currentElement = Element.fromValue(localName);
        switch (currentElement) {
            case ITEM:
                item = new RssItem();
                break;
            case LAST_BUILD_DATE:
                onChannelInfoReady(item);
                break;
        }
    }

    @Override
    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        String elementsCharacters = currentCharacters.toString();
        currentCharacters.setLength(0); // reset for reuse
        angleBracketMatched = false;
        Element currentElement = Element.fromValue(localName);
        switch (currentElement) {
            case TITLE:
                item.setTitle(elementsCharacters);
                break;
            case LINK:
                item.setLink(elementsCharacters);
                break;
            case DESCRIPTION:
                item.setDescription(elementsCharacters);
                break;
            case PUBDATE:
                try {
                    item.setDate(formatter.parse(elementsCharacters.trim()));
                } catch (ParseException e) {
                    onException(e);
                }
                break;
            case ITEM:
                onItemReady(item);
                break;
        }
    }

    @Override
    public void characters(char ch[], int start, int length) {
        // this method can be called several times subsequently (one text entry
        // is divided into parts), so we should check if angle bracket wasn't
        // already found (we truncate html banners)
        if (!angleBracketMatched) {
            Matcher matcher = Pattern.compile("<").matcher(CharBuffer.wrap(ch, start, length));
            if (matcher.find()) {
                length = matcher.start();
                angleBracketMatched = true;
            }
            currentCharacters.append(ch, start, length);
        }
    }

    private enum Element {
        ITEM("item"), TITLE("title"), DESCRIPTION("description"), LINK("link"), PUBDATE("pubDate"), OTHER(
                "other"), LAST_BUILD_DATE("lastBuildDate");

        String value;

        Element(String value) {
            this.value = value;
        }

        static Element fromValue(String value) {
            for (Element t : Element.values()) {
                if (t.value.equals(value)) {
                    return t;
                }
            }
            return OTHER;
        }
    }

}
